module ZCM

# AbstractZcmType functions
export encode,
       decode,
       getHash,
       fieldnames,
       constfieldnames
# Zcm functions
export Zcm,
       good,
       strerrno,
       subscribe,
       unsubscribe,
       publish,
       pause,
       resume,
       flush,
       start,
       stop,
       handle,
       handle_nonblock,
       set_queue_size,
       write_topology,
       read_bits,
       write_bits,
       LogEvent,
       LogFile,
       read_next_event,
       read_prev_event,
       read_event_at_offset,
       write_event

import Base: flush,
             fieldnames,
             unsafe_convert

@static if VERSION < v"0.7.0-"
    import Base: start
end

abstract type AbstractZcmType end

@static if VERSION < v"0.7.0-"
    Nothing = Void
    findall = find
end


# Function stubs. Autogenerated ZCM types will extend these functions
# with new methods.
function encode(msg::AbstractZcmType) end
function decode(::Type{AbstractZcmType}, data::Vector{UInt8}) end
function getHash(::Type{AbstractZcmType}) end
function _get_hash_recursive(::Type{AbstractZcmType}, parents::Array{String}) end
function _encode_one(msg::AbstractZcmType, buf) end
function _decode_one(::Type{AbstractZcmType}, buf) end
function fieldnames(::Type{AbstractZcmType}) end
function constfieldnames(::Type{AbstractZcmType}) end
# TODO: would be nice to have getEncodedSize() and _getEncodedSizeNoHash()


# Note: Julia requires that the memory layout of the C structs is consistent
#       between their definitions in zcm headers and this file

# C ptr types for types that we don't need the internals of
module Native

@static if VERSION < v"0.7.0-"
    Nothing = Void
    findall = find
end

mutable struct Zcm
end

mutable struct Sub
end

mutable struct UvSub
end

struct  RecvBuf
    recv_utime ::Int64
    zcm        ::Ptr{Nothing}
    # TODO: This makes the assumption that char in C is 8 bits, which is not required to be true
    data       ::Ptr{UInt8}
    data_size  ::UInt32
end

mutable struct EventLog
end

mutable struct EventLogEvent
    eventnum  ::Int64
    timestamp ::Int64
    chanlen   ::Int32
    datalen   ::Int32
    channel   ::Ptr{UInt8}
    data      ::Ptr{UInt8}
end

end

using .Native: RecvBuf

"""
The Subscription type contains the Julia handler and also all of the
various C pointers to the libuv handlers.
"""
struct Subscription{F}
    jl_handler::F
    c_handler::Ptr{Nothing}
    uv_wrapper::Ptr{Native.UvSub}
    uv_handler::Ptr{Nothing}
    native_sub::Ptr{Native.Sub}
end

mutable struct Zcm
    zcm::Ptr{Native.Zcm}
    subscriptions::Vector{Subscription}

    function Zcm(url::AbstractString = "")
        pointer = ccall(("zcm_create", "libzcm"), Ptr{Native.Zcm}, (Cstring,), url);
        instance = new(pointer, Subscription[])

        @static if VERSION < v"0.7.0-"
            finalizer(instance, destroy)
        else
            finalizer(destroy, instance)
        end

        return instance
    end
end

function destroy(zcm::Zcm)
    if zcm.zcm != C_NULL
        ccall(("zcm_destroy", "libzcm"), Nothing,
              (Ptr{Native.Zcm},), zcm)
        zcm.zcm = C_NULL
    end
end

# Defines the conversion when we pass this to a C function expecting a pointer
unsafe_convert(::Type{Ptr{Native.Zcm}}, zcm::Zcm) = zcm.zcm

function good(zcm::Zcm)
    zcm.zcm != C_NULL
end

function strerrno(err::Int)
    val = ccall(("zcm_strerrno", "libzcm"), Cstring, (Cint,), Cint(err))
    if (val == C_NULL)
        return "unable to get strerrno"
    else
        return unsafe_string(val)
    end
end

function handler_wrapper(rbuf::Native.RecvBuf, channelbytes::Cstring, handler)
    channel = unsafe_string(channelbytes)
    msgdata = unsafe_wrap(Vector{UInt8}, rbuf.data, rbuf.data_size)
    handler(rbuf, channel, msgdata)
    return nothing
end

function typed_handler(handler, msgtype::Type{T}, args...) where T <: AbstractZcmType
    (rbuf, channel, msgdata) -> handler(rbuf, channel, decode(T, msgdata), args...)
end

function typed_handler(handler, msgtype::Type{Nothing}, args...)
    (rbuf, channel, msgdata) -> handler(rbuf, channel, msgdata, args...)
end

@static if VERSION < v"0.7.0-"
    sub_handler(::Type{T}) where {T} = cfunction(handler_wrapper, Nothing, (Ref{Native.RecvBuf}, Cstring, Ref{T}))
else
    sub_handler(::Type{T}) where {T} = @cfunction(handler_wrapper, Nothing, (Ref{Native.RecvBuf}, Cstring, Ref{T}))
end

"""
    subscribe(zcm::Zcm, channel::AbstractString, handler, additional_args...)

Adds a subscription using ZCM object `zcm` on the given channel. The `handler`
must be a function or callable object, and will be called with:

    handler(rbuf::RecvBuf, channel::String, msgdata::Vector{UInt8})

If additional arguments are supplied to `subscribe()` after the handler,
then they will also be passed to the handler each time it is called. So:

    subscribe(zcm, channel, handler, X, Y, Z)

will cause `handler()` to be invoked with:

    handler(rbuf, channel, msgdata, X, Y, Z)
"""
function subscribe(zcm::Zcm, channel::AbstractString,
                   handler,
                   msgtype=Nothing,
                   additional_args...)
    callback = typed_handler(handler, msgtype, additional_args...)
    c_handler = sub_handler(typeof(callback))
    uv_wrapper = ccall(("uv_zcm_msg_handler_create", "libzcmjulia"),
                       Ptr{Native.UvSub},
                       (Ptr{Nothing}, Ptr{Nothing}),
                       c_handler, Ref(callback))
    uv_handler = cglobal(("uv_zcm_msg_handler_trigger", "libzcmjulia"))
    try_sub = () -> ccall(("zcm_try_subscribe", "libzcm"), Ptr{Native.Sub},
                          (Ptr{Native.Zcm}, Cstring, Ptr{Nothing}, Ptr{Native.UvSub}),
                          zcm, channel, uv_handler, uv_wrapper)
    csub = Ptr{Native.Sub}(C_NULL)
    while (true)
        csub = try_sub()
        if (csub == C_NULL)
            yield()
        else
            break
        end
    end
    sub = Subscription(callback, c_handler, uv_wrapper, uv_handler, csub)
    push!(zcm.subscriptions, sub)
    return sub
end

function unsubscribe(zcm::Zcm, sub::Subscription)
    try_unsub = () -> ccall(("zcm_try_unsubscribe", "libzcm"), Cint,
                            (Ptr{Native.Zcm}, Ptr{Native.Sub}), zcm, sub.native_sub)
    ret = Cint(0)
    while (true)
        ret = try_unsub()
        if (ret == -2)
            yield()
        else
            break
        end
    end
    ccall(("uv_zcm_msg_handler_destroy", "libzcmjulia"), Nothing,
          (Ptr{Native.UvSub},), sub.uv_wrapper)
    deleteat!(zcm.subscriptions, findall(s -> s == sub, zcm.subscriptions))
    return ret
end

function publish(zcm::Zcm, channel::AbstractString, data::Vector{UInt8})
    ccall(("zcm_publish", "libzcm"), Cint,
          (Ptr{Native.Zcm}, Cstring, Ptr{Nothing}, UInt32),
          zcm, convert(String, channel), data, length(data))
end

function publish(zcm::Zcm, channel::AbstractString, msg::AbstractZcmType)
    publish(zcm, channel, encode(msg))
end

function pause(zcm::Zcm)
    ccall(("zcm_pause", "libzcm"), Nothing, (Ptr{Native.Zcm},), zcm)
end

function resume(zcm::Zcm)
    ccall(("zcm_resume", "libzcm"), Nothing, (Ptr{Native.Zcm},), zcm)
end

function flush(zcm::Zcm)
    while (true)
        ret = ccall(("zcm_try_flush", "libzcm"), Cint, (Ptr{Native.Zcm},), zcm)
        if (ret == Cint(0))
            break
        else
            yield()
        end
    end
end

function start(zcm::Zcm)
    @warn "Threaded interface was partially broken by Julia 1.6 : you cannot put printouts in handlers"
    ccall(("zcm_start", "libzcm"), Nothing, (Ptr{Native.Zcm},), zcm)
end

function stop(zcm::Zcm)
    while (true)
        ret = ccall(("zcm_try_stop", "libzcm"), Cint, (Ptr{Native.Zcm},), zcm)
        if (ret == Cint(0))
            break
        else
            yield()
        end
    end
end

function handle(zcm::Zcm)
    ccall(("zcm_handle", "libzcm"), Cint, (Ptr{Native.Zcm},), zcm)
end

function handle_nonblock(zcm::Zcm)
    ccall(("zcm_handle_nonblock", "libzcm"), Cint, (Ptr{Native.Zcm},), zcm)
end

function set_queue_size(zcm::Zcm, num::Integer)
    sz = UInt32(num)
    while (true)
        ret = ccall(("zcm_try_set_queue_size", "libzcm"), Cint,
                    (Ptr{Native.Zcm}, UInt32), zcm, sz)
        if (ret == Cint(0))
            break
        else
            yield()
        end
    end
end

function write_topology(zcm::Zcm, name::AbstractString)
    ccall(("zcm_write_topology", "libzcm"), Cint,
          (Ptr{Native.Zcm}, Cstring),
          zcm, convert(String, name))
end

function read_bits(T::Type, buf::IOBuffer, numbits::Int, offset_bit::Int, signExtend::Bool)
    ret = T(0)
    bits_left = numbits
    while (bits_left > 0)
        available_bits = 8 - offset_bit
        bits_covered = available_bits < bits_left ? available_bits : bits_left
        mask = ((1 << bits_covered) - 1) << (8 - bits_covered - offset_bit)
        payload::UInt8 = (peek(buf) & mask) << offset_bit
        shift = 8 - bits_left
        if (bits_left == numbits)
            if (shift < 0)
                if signExtend
                    ret = T(reinterpret(Int8, payload)) << -shift
                else
                    ret = T(payload) << -shift
                end
            else
                if signExtend
                    ret = T(reinterpret(Int8, payload)) >> shift
                else
                    ret = payload >>> shift
                end
            end
        else
            if (shift < 0)
                ret |= T(payload) << -shift
            else
                if T == Int8
                    ret |= reinterpret(Int8, payload >>> shift)
                else
                    ret |= T(payload) >>> shift
                end
            end
        end
        bits_left -= bits_covered
        offset_bit += bits_covered
        if (offset_bit == 8)
            offset_bit = 0
            read(buf, 1)
        end
    end

    return offset_bit, ret
end

function write_bits(buf::IOBuffer, value::Any, numbits::Int, byte_in_progress::UInt8, offset_bit::Int)
    bits_left = numbits;
    while (bits_left > 0)
        mask::UInt64 = (1 << bits_left) - 1;
        shift = offset_bit + bits_left - 8;
        if (shift < 0)
            byte_in_progress |= UInt8((value & mask) << -shift)
            offset_bit += bits_left;
            return byte_in_progress, offset_bit
        end
        byte_in_progress |= UInt8((value & mask) >> shift)
        write(buf, byte_in_progress);
        bits_left = shift;
        offset_bit = 0;
        byte_in_progress = UInt8(0)
    end
    return byte_in_progress, offset_bit
end

mutable struct LogEvent
    # These values only valid for events read from a log
    event   ::Ptr{Native.EventLogEvent}
    num     ::Int64

    # These values valid for user created events or events read from a log
    utime   ::Int64
    channel ::String
    data    ::Array{UInt8}

    # Bookkeeping
    valid   ::Bool

    function LogEvent(channel::AbstractString, msg::AbstractZcmType, utime::Int64)
        instance = new()

        instance.event   = C_NULL
        instance.num     = 0
        instance.utime   = utime
        instance.channel = convert(String, channel)
        instance.data    = encode(msg)
        instance.valid   = true

        @static if VERSION < v"0.7.0-"
            finalizer(instance, destroy)
        else
            finalizer(destroy, instance)
        end


        return instance
    end

    function LogEvent(event::Ptr{Native.EventLogEvent})
        instance = new()
        instance.event = event
        instance.valid = false

        if (event != C_NULL)
            loadedEvent = unsafe_load(event)

            instance.num   = loadedEvent.eventnum
            instance.utime = loadedEvent.timestamp
            if (loadedEvent.channel != C_NULL)
                instance.channel = unsafe_string(loadedEvent.channel, loadedEvent.chanlen)
            else
                instance.channel = ""
            end
            if (loadedEvent.data != C_NULL)
                instance.data    = unsafe_wrap(Array, loadedEvent.data, loadedEvent.datalen)
            else
                instance.data    = []
            end

            instance.valid = true
        end

        # user can force cleanup of their instance by calling `finalize(zcm)`
        @static if VERSION < v"0.7.0-"
            finalizer(instance, destroy)
        else
            finalizer(destroy, instance)
        end

        return instance
    end
end

function destroy(event::LogEvent)
    if (event.event != C_NULL)
        ccall(("zcm_eventlog_free_event", "libzcm"), Nothing,
              (Ptr{Native.EventLogEvent},), event.event)
        event.event = C_NULL
    end
end

function good(event::LogEvent)
    return event.valid
end

mutable struct LogFile
    eventLog::Ptr{Native.EventLog}

    """
    path = the filesystem path of the log
    mode = "w", "r", or "a" for write, read, or append respectively
    """
    function LogFile(path::AbstractString, mode::AbstractString)
        instance = new()
        instance.eventLog = ccall(("zcm_eventlog_create", "libzcm"), Ptr{Native.EventLog},
                                  (Cstring, Cstring), path, mode)

        # user can force cleanup of their instance by calling `finalize(zcm)`
        @static if VERSION < v"0.7.0-"
            finalizer(instance, destroy)
        else
            finalizer(destroy, instance)
        end

        return instance
    end
end

function destroy(log::LogFile)
    if (log.eventLog != C_NULL)
        ccall(("zcm_eventlog_destroy", "libzcm"), Nothing,
              (Ptr{Native.EventLog},), log.eventLog)
        log.eventLog = C_NULL
    end
end

function good(lf::LogFile)
    return lf.eventLog != C_NULL
end

# TODO: we could actually make all zcmtypes register their hash somewhere and allow the
#       read_event functions to return an actual zcmtype
function read_next_event(lf::LogFile)
    event = ccall(("zcm_eventlog_read_next_event", "libzcm"), Ptr{Native.EventLogEvent},
                  (Ptr{Native.EventLog},), lf.eventLog)
    return LogEvent(event)
end

function read_prev_event(lf::LogFile)
    event = ccall(("zcm_eventlog_read_prev_event", "libzcm"), Ptr{Native.EventLogEvent},
                  (Ptr{Native.EventLog},), lf.eventLog)
    return LogEvent(event)
end

function read_event_at_offset(lf::LogFile, offset::Int64)
    event = ccall(("zcm_eventlog_read_event_at_offset", "libzcm"), Ptr{Native.EventLogEvent},
                  (Ptr{Native.EventLog}, Int64), lf.eventLog, offset)
    return LogEvent(event)
end

function write_event(lf::LogFile, event::LogEvent)
    nativeEvent = Native.EventLogEvent(event.num,
                                       event.utime,
                                       length(event.channel),
                                       length(event.data),
                                       pointer(event.channel),
                                       pointer(event.data))
    return ccall(("zcm_eventlog_write_event", "libzcm"), Cint,
                 (Ptr{Native.EventLog}, Ref{Native.EventLogEvent}),
                 lf.eventLog, nativeEvent)
end

end # module ZCM
